// ==========================================================================
// Plyr
// plyr.js v1.3.6
// https://github.com/selz/plyr
// License: The MIT License (MIT)
// ==========================================================================
// Credits: http://paypal.github.io/accessible-html5-video-player/
// ==========================================================================

(function (api) {
  'use strict';
  /*global YT*/

  // Globals
  var fullscreen, config, callbacks = {
    youtube: []
  };

  // Default config
  var defaults = {
    enabled: true,
    debug: false,
    seekTime: 10,
    volume: 5,
    click: true,
    tooltips: true,
    displayDuration: true,
    iconPrefix: 'icon',
    selectors: {
      container: '.player',
      controls: '.player-controls',
      labels: '[data-player] .sr-only, label .sr-only',
      buttons: {
        seek: '[data-player="seek"]',
        play: '[data-player="play"]',
        pause: '[data-player="pause"]',
        restart: '[data-player="restart"]',
        rewind: '[data-player="rewind"]',
        forward: '[data-player="fast-forward"]',
        mute: '[data-player="mute"]',
        volume: '[data-player="volume"]',
        captions: '[data-player="captions"]',
        fullscreen: '[data-player="fullscreen"]'
      },
      progress: {
        container: '.player-progress',
        buffer: '.player-progress-buffer',
        played: '.player-progress-played'
      },
      captions: '.player-captions',
      currentTime: '.player-current-time',
      duration: '.player-duration'
    },
    classes: {
      videoWrapper: 'player-video-wrapper',
      embedWrapper: 'player-video-embed',
      type: 'player-{0}',
      stopped: 'stopped',
      playing: 'playing',
      muted: 'muted',
      loading: 'loading',
      tooltip: 'player-tooltip',
      hidden: 'sr-only',
      hover: 'player-hover',
      captions: {
        enabled: 'captions-enabled',
        active: 'captions-active'
      },
      fullscreen: {
        enabled: 'fullscreen-enabled',
        active: 'fullscreen-active',
        hideControls: 'fullscreen-hide-controls'
      }
    },
    captions: {
      defaultActive: false
    },
    fullscreen: {
      enabled: true,
      fallback: true,
      hideControls: true
    },
    storage: {
      enabled: true,
      key: 'plyr_volume'
    },
    controls: ['restart', 'rewind', 'play', 'fast-forward', 'current-time',
      'duration', 'mute', 'volume', /*'captions',*/ 'fullscreen'],
    i18n: {
      restart: '重新播放',
      rewind: '后退{seektime}秒',
      play: '播放',
      pause: '暂停',
      forward: '快进{seektime}秒',
      played: '播放中',
      buffered: '缓冲中',
      currentTime: '当前时间',
      duration: '持续时间',
      volume: '音量',
      toggleMute: '静音',
      toggleCaptions: '字幕',
      toggleFullscreen: '全屏'
    }
  };

  // Build the default HTML
  function _buildControls() {
    // Open and add the progress and seek elements
    var html = [
      '<div class="player-controls">',
      '<div class="player-progress">',
      '<label for="seek{id}" class="sr-only">Seek</label>',
      '<input id="seek{id}" class="player-progress-seek" type="range" min="0" max="100" step="0.5" value="0" data-player="seek">',
      '<progress class="player-progress-played" max="100" value="0">',
      '<span>0</span>% ' + config.i18n.played,
      '</progress>',
      '<progress class="player-progress-buffer" max="100" value="0">',
      '<span>0</span>% ' + config.i18n.buffered,
      '</progress>',
      '</div>',
      '<span class="player-controls-left">'];

    // Restart button
    if (_inArray(config.controls, 'restart')) {
      html.push(
          '<button type="button" data-player="restart">',
          '<svg><use xlink:href="#' + config.iconPrefix + '-restart" /></svg>',
          '<span class="sr-only">' + config.i18n.restart + '</span>',
          '</button>'
      );
    }

    // Rewind button
    if (_inArray(config.controls, 'rewind')) {
      html.push(
          '<button type="button" data-player="rewind">',
          '<svg><use xlink:href="#' + config.iconPrefix + '-rewind" /></svg>',
          '<span class="sr-only">' + config.i18n.rewind + '</span>',
          '</button>'
      );
    }

    // Play/pause button
    if (_inArray(config.controls, 'play')) {
      html.push(
          '<button type="button" data-player="play">',
          '<svg><use xlink:href="#' + config.iconPrefix + '-play" /></svg>',
          '<span class="sr-only">' + config.i18n.play + '</span>',
          '</button>',
          '<button type="button" data-player="pause">',
          '<svg><use xlink:href="#' + config.iconPrefix + '-pause" /></svg>',
          '<span class="sr-only">' + config.i18n.pause + '</span>',
          '</button>'
      );
    }

    // Fast forward button
    if (_inArray(config.controls, 'fast-forward')) {
      html.push(
          '<button type="button" data-player="fast-forward">',
          '<svg><use xlink:href="#' + config.iconPrefix
          + '-fast-forward" /></svg>',
          '<span class="sr-only">' + config.i18n.forward + '</span>',
          '</button>'
      );
    }

    // Media current time display
    if (_inArray(config.controls, 'current-time')) {
      html.push(
          '<span class="player-time">',
          '<span class="sr-only">' + config.i18n.currentTime + '</span>',
          '<span class="player-current-time">00:00</span>',
          '</span>'
      );
    }

    // Media duration display
    if (_inArray(config.controls, 'duration')) {
      html.push(
          '<span class="player-time">',
          '<span class="sr-only">' + config.i18n.duration + '</span>',
          '<span class="player-duration">00:00</span>',
          '</span>'
      );
    }

    // Close left controls
    html.push(
        '</span>',
        '<span class="player-controls-right">'
    );

    // Toggle mute button
    if (_inArray(config.controls, 'mute')) {
      html.push(
          '<button type="button" data-player="mute">',
          '<svg class="icon-muted"><use xlink:href="#' + config.iconPrefix
          + '-muted" /></svg>',
          '<svg><use xlink:href="#' + config.iconPrefix + '-volume" /></svg>',
          '<span class="sr-only">' + config.i18n.toggleMute + '</span>',
          '</button>'
      );
    }

    // Volume range control
    if (_inArray(config.controls, 'volume')) {
      html.push(
          '<label for="volume{id}" class="sr-only">' + config.i18n.volume
          + '</label>',
          '<input id="volume{id}" class="player-volume" type="range" min="0" max="10" value="5" data-player="volume">'
      );
    }

    // Toggle captions button
    if (_inArray(config.controls, 'captions')) {
      html.push(
          '<button type="button" data-player="captions">',
          '<svg class="icon-captions-on"><use xlink:href="#' + config.iconPrefix
          + '-captions-on" /></svg>',
          '<svg><use xlink:href="#' + config.iconPrefix
          + '-captions-off" /></svg>',
          '<span class="sr-only">' + config.i18n.toggleCaptions + '</span>',
          '</button>'
      );
    }

    // Toggle fullscreen button
    if (_inArray(config.controls, 'fullscreen')) {
      html.push(
          '<button type="button" data-player="fullscreen">',
          '<svg class="icon-exit-fullscreen"><use xlink:href="#'
          + config.iconPrefix + '-exit-fullscreen" /></svg>',
          '<svg><use xlink:href="#' + config.iconPrefix
          + '-enter-fullscreen" /></svg>',
          '<span class="sr-only">' + config.i18n.toggleFullscreen + '</span>',
          '</button>'
      );
    }

    // Close everything
    html.push(
        '</span>',
        '</div>'
    );

    return html.join('');
  }

  // Debugging
  function _log(text, error) {
    if (config.debug && window.console) {
      console[(error ? 'error' : 'log')](text);
    }
  }

  // Credits: http://paypal.github.io/accessible-html5-video-player/
  // Unfortunately, due to mixed support, UA sniffing is required
  function _browserSniff() {
    var nAgt = navigator.userAgent,
        name = navigator.appName,
        fullVersion = '' + parseFloat(navigator.appVersion),
        majorVersion = parseInt(navigator.appVersion, 10),
        nameOffset,
        verOffset,
        ix;

    // MSIE 11
    if ((navigator.appVersion.indexOf('Windows NT') !== -1)
        && (navigator.appVersion.indexOf('rv:11') !== -1)) {
      name = 'IE';
      fullVersion = '11;';
    }
    // MSIE
    else if ((verOffset = nAgt.indexOf('MSIE')) !== -1) {
      name = 'IE';
      fullVersion = nAgt.substring(verOffset + 5);
    }
    // Chrome
    else if ((verOffset = nAgt.indexOf('Chrome')) !== -1) {
      name = 'Chrome';
      fullVersion = nAgt.substring(verOffset + 7);
    }
    // Safari
    else if ((verOffset = nAgt.indexOf('Safari')) !== -1) {
      name = 'Safari';
      fullVersion = nAgt.substring(verOffset + 7);
      if ((verOffset = nAgt.indexOf('Version')) !== -1) {
        fullVersion = nAgt.substring(verOffset + 8);
      }
    }
    // Firefox
    else if ((verOffset = nAgt.indexOf('Firefox')) !== -1) {
      name = 'Firefox';
      fullVersion = nAgt.substring(verOffset + 8);
    }
    // In most other browsers, 'name/version' is at the end of userAgent
    else if ((nameOffset = nAgt.lastIndexOf(' ') + 1)
        < (verOffset = nAgt.lastIndexOf('/'))) {
      name = nAgt.substring(nameOffset, verOffset);
      fullVersion = nAgt.substring(verOffset + 1);

      if (name.toLowerCase() == name.toUpperCase()) {
        name = navigator.appName;
      }
    }
    // Trim the fullVersion string at semicolon/space if present
    if ((ix = fullVersion.indexOf(';')) !== -1) {
      fullVersion = fullVersion.substring(0, ix);
    }
    if ((ix = fullVersion.indexOf(' ')) !== -1) {
      fullVersion = fullVersion.substring(0, ix);
    }
    // Get major version
    majorVersion = parseInt('' + fullVersion, 10);
    if (isNaN(majorVersion)) {
      fullVersion = '' + parseFloat(navigator.appVersion);
      majorVersion = parseInt(navigator.appVersion, 10);
    }

    // Return data
    return {
      name: name,
      version: majorVersion,
      ios: /(iPad|iPhone|iPod)/g.test(navigator.platform)
    };
  }

  // Check for mime type support against a player instance
  // Credits: http://diveintohtml5.info/everything.html
  // Related: http://www.leanbackplayer.com/test/h5mt.html
  function _supportMime(player, mimeType) {
    var media = player.media;

    // Only check video types for video players
    if (player.type == 'video') {
      // Check type
      switch (mimeType) {
        case 'video/webm':
          return !!(media.canPlayType && media.canPlayType(
              'video/webm; codecs="vp8, vorbis"').replace(/no/, ''));
        case 'video/mp4':
          return !!(media.canPlayType && media.canPlayType(
              'video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''));
        case 'video/ogg':
          return !!(media.canPlayType && media.canPlayType(
              'video/ogg; codecs="theora"').replace(/no/, ''));
      }
    }

    // Only check audio types for audio players
    else if (player.type == 'audio') {
      // Check type
      switch (mimeType) {
        case 'audio/mpeg':
          return !!(media.canPlayType && media.canPlayType(
              'audio/mpeg;').replace(/no/, ''));
        case 'audio/ogg':
          return !!(media.canPlayType && media.canPlayType(
              'audio/ogg; codecs="vorbis"').replace(/no/, ''));
        case 'audio/wav':
          return !!(media.canPlayType && media.canPlayType(
              'audio/wav; codecs="1"').replace(/no/, ''));
      }
    }

    // If we got this far, we're stuffed
    return false;
  }

  // Inject a script
  function _injectScript(source) {
    if (document.querySelectorAll('script[src="' + source + '"]').length) {
      return;
    }

    var tag = document.createElement('script');
    tag.src = source;
    var firstScriptTag = document.getElementsByTagName('script')[0];
    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
  }

  // Element exists in an array
  function _inArray(haystack, needle) {
    return Array.prototype.indexOf && (haystack.indexOf(needle) != -1);
  }

  // Replace all
  function _replaceAll(string, find, replace) {
    return string.replace(
        new RegExp(find.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g, '\\$1'), 'g'),
        replace);
  }

  // Wrap an element
  function _wrap(elements, wrapper) {
    // Convert `elements` to an array, if necessary.
    if (!elements.length) {
      elements = [elements];
    }

    // Loops backwards to prevent having to clone the wrapper on the
    // first element (see `child` below).
    for (var i = elements.length - 1; i >= 0; i--) {
      var child = (i > 0) ? wrapper.cloneNode(true) : wrapper;
      var element = elements[i];

      // Cache the current parent and sibling.
      var parent = element.parentNode;
      var sibling = element.nextSibling;

      // Wrap the element (is automatically removed from its current
      // parent).
      child.appendChild(element);

      // If the element had a sibling, insert the wrapper before
      // the sibling to maintain the HTML structure; otherwise, just
      // append it to the parent.
      if (sibling) {
        parent.insertBefore(child, sibling);
      } else {
        parent.appendChild(child);
      }
    }
  }

  // Unwrap an element
  // http://plainjs.com/javascript/manipulation/unwrap-a-dom-element-35/
  function _unwrap(wrapper) {
    // Get the element's parent node
    var parent = wrapper.parentNode;

    // Move all children out of the element
    while (wrapper.firstChild) {
      parent.insertBefore(wrapper.firstChild, wrapper);
    }

    // Remove the empty element
    parent.removeChild(wrapper);
  }

  // Remove an element
  function _remove(element) {
    element.parentNode.removeChild(element);
  }

  // Prepend child
  function _prependChild(parent, element) {
    parent.insertBefore(element, parent.firstChild);
  }

  // Set attributes
  function _setAttributes(element, attributes) {
    for (var key in attributes) {
      element.setAttribute(key, attributes[key]);
    }
  }

  // Toggle class on an element
  function _toggleClass(element, name, state) {
    if (element) {
      if (element.classList) {
        element.classList[state ? 'add' : 'remove'](name);
      } else {
        var className = (' ' + element.className + ' ').replace(/\s+/g,
            ' ').replace(' ' + name + ' ', '');
        element.className = className + (state ? ' ' + name : '');
      }
    }
  }

  // Toggle event
  function _toggleHandler(element, events, callback, toggle) {
    var eventList = events.split(' ');

    // If a nodelist is passed, call itself on each node
    if (element instanceof NodeList) {
      for (var x = 0; x < element.length; x++) {
        if (element[x] instanceof Node) {
          _toggleHandler(element[x], arguments[1], arguments[2], arguments[3]);
        }
      }
      return;
    }

    // If a single node is passed, bind the event listener
    for (var i = 0; i < eventList.length; i++) {
      element[toggle ? 'addEventListener' : 'removeEventListener'](eventList[i],
          callback, false);
    }
  }

  // Bind event
  function _on(element, events, callback) {
    if (element) {
      _toggleHandler(element, events, callback, true);
    }
  }

  // Unbind event
  function _off(element, events, callback) {
    if (element) {
      _toggleHandler(element, events, callback, false);
    }
  }

  // Trigger event
  function _triggerEvent(element, event) {
    // Create faux event
    var fauxEvent = document.createEvent('MouseEvents');

    // Set the event type
    fauxEvent.initEvent(event, true, true);

    // Dispatch the event
    element.dispatchEvent(fauxEvent);
  }

  // Toggle aria-pressed state on a toggle button
  function _toggleState(target, state) {
    // Get state
    state = (typeof state === 'boolean' ? state : !target.getAttribute(
        'aria-pressed'));

    // Set the attribute on target
    target.setAttribute('aria-pressed', state);

    return state;
  }

  // Get percentage
  function _getPercentage(current, max) {
    if (current === 0 || max === 0 || isNaN(current) || isNaN(max)) {
      return 0;
    }
    return ((current / max) * 100).toFixed(2);
  }

  // Deep extend/merge two Objects
  // http://andrewdupont.net/2009/08/28/deep-extending-objects-in-javascript/
  // Removed call to arguments.callee (used explicit function name instead)
  function _extend(destination, source) {
    for (var property in source) {
      if (source[property] && source[property].constructor
          && source[property].constructor === Object) {
        destination[property] = destination[property] || {};
        _extend(destination[property], source[property]);
      } else {
        destination[property] = source[property];
      }
    }
    return destination;
  }

  // Fullscreen API
  function _fullscreen() {
    var fullscreen = {
          supportsFullScreen: false,
          isFullScreen: function () {
            return false;
          },
          requestFullScreen: function () {
          },
          cancelFullScreen: function () {
          },
          fullScreenEventName: '',
          element: null,
          prefix: ''
        },
        browserPrefixes = 'webkit moz o ms khtml'.split(' ');

    // Check for native support
    if (typeof document.cancelFullScreen !== 'undefined') {
      fullscreen.supportsFullScreen = true;
    } else {
      // Check for fullscreen support by vendor prefix
      for (var i = 0, il = browserPrefixes.length; i < il; i++) {
        fullscreen.prefix = browserPrefixes[i];

        if (typeof document[fullscreen.prefix + 'CancelFullScreen']
            !== 'undefined') {
          fullscreen.supportsFullScreen = true;
          break;
        }
        // Special case for MS (when isn't it?)
        else if (typeof document.msExitFullscreen !== 'undefined'
            && document.msFullscreenEnabled) {
          fullscreen.prefix = 'ms';
          fullscreen.supportsFullScreen = true;
          break;
        }
      }
    }

    // Update methods to do something useful
    if (fullscreen.supportsFullScreen) {
      // Yet again Microsoft awesomeness,
      // Sometimes the prefix is 'ms', sometimes 'MS' to keep you on your toes
      fullscreen.fullScreenEventName = (fullscreen.prefix == 'ms'
          ? 'MSFullscreenChange' : fullscreen.prefix + 'fullscreenchange');

      fullscreen.isFullScreen = function (element) {
        if (typeof element === 'undefined') {
          element = document.body;
        }
        switch (this.prefix) {
          case '':
            return document.fullscreenElement == element;
          case 'moz':
            return document.mozFullScreenElement == element;
          default:
            return document[this.prefix + 'FullscreenElement'] == element;
        }
      };
      fullscreen.requestFullScreen = function (element) {
        if (typeof element === 'undefined') {
          element = document.body;
        }
        return (this.prefix === '') ? element.requestFullScreen()
            : element[this.prefix + (this.prefix == 'ms' ? 'RequestFullscreen'
                : 'RequestFullScreen')]();
      };
      fullscreen.cancelFullScreen = function () {
        return (this.prefix === '') ? document.cancelFullScreen()
            : document[this.prefix + (this.prefix == 'ms' ? 'ExitFullscreen'
                : 'CancelFullScreen')]();
      };
      fullscreen.element = function () {
        return (this.prefix === '') ? document.fullscreenElement
            : document[this.prefix + 'FullscreenElement'];
      };
    }

    return fullscreen;
  }

  // Local storage
  function _storage() {
    var storage = {
      supported: (function () {
        try {
          return 'localStorage' in window && window.localStorage !== null;
        } catch (e) {
          return false;
        }
      })()
    };
    return storage;
  }

  // Player instance
  function Plyr(container) {
    var player = this;
    player.container = container;

    // Captions functions
    // Seek the manual caption time and update UI
    function _seekManualCaptions(time) {
      // If it's not video, or we're using textTracks, bail.
      if (player.usingTextTracks || player.type !== 'video'
          || !player.supported.full) {
        return;
      }

      // Reset subcount
      player.subcount = 0;

      // Check time is a number, if not use currentTime
      // IE has a bug where currentTime doesn't go to 0
      // https://twitter.com/Sam_Potts/status/573715746506731521
      time = typeof time === 'number' ? time : player.media.currentTime;

      while (_timecodeMax(player.captions[player.subcount][0]) < time.toFixed(
          1)) {
        player.subcount++;
        if (player.subcount > player.captions.length - 1) {
          player.subcount = player.captions.length - 1;
          break;
        }
      }

      // Check if the next caption is in the current time range
      if (player.media.currentTime.toFixed(1) >= _timecodeMin(
          player.captions[player.subcount][0]) &&
          player.media.currentTime.toFixed(1) <= _timecodeMax(
              player.captions[player.subcount][0])) {
        player.currentCaption = player.captions[player.subcount][1];

        // Trim caption text
        var content = player.currentCaption.trim();

        // Render the caption (only if changed)
        if (player.captionsContainer.innerHTML != content) {
          // Empty caption
          // Otherwise NVDA reads it twice
          player.captionsContainer.innerHTML = '';

          // Set new caption text
          player.captionsContainer.innerHTML = content;
        }
      } else {
        player.captionsContainer.innerHTML = '';
      }
    }

    // Display captions container and button (for initialization)
    function _showCaptions() {
      // If there's no caption toggle, bail
      if (!player.buttons.captions) {
        return;
      }

      _toggleClass(player.container, config.classes.captions.enabled, true);

      if (config.captions.defaultActive) {
        _toggleClass(player.container, config.classes.captions.active, true);
        _toggleState(player.buttons.captions, true);
      }
    }

    // Utilities for caption time codes
    function _timecodeMin(tc) {
      var tcpair = [];
      tcpair = tc.split(' --> ');
      return _subTcSecs(tcpair[0]);
    }

    function _timecodeMax(tc) {
      var tcpair = [];
      tcpair = tc.split(' --> ');
      return _subTcSecs(tcpair[1]);
    }

    function _subTcSecs(tc) {
      if (tc === null || tc === undefined) {
        return 0;
      } else {
        var tc1 = [],
            tc2 = [],
            seconds;
        tc1 = tc.split(',');
        tc2 = tc1[0].split(':');
        seconds = Math.floor(tc2[0] * 60 * 60) + Math.floor(tc2[1] * 60)
            + Math.floor(tc2[2]);
        return seconds;
      }
    }

    // Find all elements
    function _getElements(selector) {
      return player.container.querySelectorAll(selector);
    }

    // Find a single element
    function _getElement(selector) {
      return _getElements(selector)[0];
    }

    // Determine if we're in an iframe
    function _inFrame() {
      try {
        return window.self !== window.top;
      } catch (e) {
        return true;
      }
    }

    // Insert controls
    function _injectControls() {
      // Make a copy of the html
      var html = config.html;

      // Insert custom video controls
      _log('Injecting custom controls.');

      // If no controls are specified, create default
      if (!html) {
        html = _buildControls();
      }

      // Replace seek time instances
      html = _replaceAll(html, '{seektime}', config.seekTime);

      // Replace all id references with random numbers
      html = _replaceAll(html, '{id}', Math.floor(Math.random() * (10000)));

      // Inject into the container
      player.container.insertAdjacentHTML('beforeend', html);

      // Setup tooltips
      if (config.tooltips) {
        var labels = _getElements(config.selectors.labels);

        for (var i = labels.length - 1; i >= 0; i--) {
          var label = labels[i];

          _toggleClass(label, config.classes.hidden, false);
          _toggleClass(label, config.classes.tooltip, true);
        }
      }
    }

    // Find the UI controls and store references
    function _findElements() {
      try {
        player.controls = _getElement(config.selectors.controls);

        // Buttons
        player.buttons = {};
        player.buttons.seek = _getElement(config.selectors.buttons.seek);
        player.buttons.play = _getElement(config.selectors.buttons.play);
        player.buttons.pause = _getElement(config.selectors.buttons.pause);
        player.buttons.restart = _getElement(config.selectors.buttons.restart);
        player.buttons.rewind = _getElement(config.selectors.buttons.rewind);
        player.buttons.forward = _getElement(config.selectors.buttons.forward);
        player.buttons.fullscreen = _getElement(
            config.selectors.buttons.fullscreen);

        // Inputs
        player.buttons.mute = _getElement(config.selectors.buttons.mute);
        player.buttons.captions = _getElement(
            config.selectors.buttons.captions);
        player.checkboxes = _getElements('[type="checkbox"]');

        // Progress
        player.progress = {};
        player.progress.container = _getElement(
            config.selectors.progress.container);

        // Progress - Buffering
        player.progress.buffer = {};
        player.progress.buffer.bar = _getElement(
            config.selectors.progress.buffer);
        player.progress.buffer.text = player.progress.buffer.bar
            && player.progress.buffer.bar.getElementsByTagName('span')[0];

        // Progress - Played
        player.progress.played = {};
        player.progress.played.bar = _getElement(
            config.selectors.progress.played);
        player.progress.played.text = player.progress.played.bar
            && player.progress.played.bar.getElementsByTagName('span')[0];

        // Volume
        player.volume = _getElement(config.selectors.buttons.volume);

        // Timing
        player.duration = _getElement(config.selectors.duration);
        player.currentTime = _getElement(config.selectors.currentTime);
        player.seekTime = _getElements(config.selectors.seekTime);

        return true;
      } catch (e) {
        _log(
            'It looks like there\'s a problem with your controls html. Bailing.',
            true);

        // Restore native video controls
        player.media.setAttribute('controls', '');

        return false;
      }
    }

    // Setup aria attribute for play
    function _setupPlayAria() {
      // If there's no play button, bail
      if (!player.buttons.play) {
        return;
      }

      // Find the current text
      var label = player.buttons.play.innerText || config.i18n.play;

      // If there's a media title set, use that for the label
      if (typeof (config.title) !== 'undefined' && config.title.length) {
        label += ', ' + config.title;
      }

      player.buttons.play.setAttribute('aria-label', label);
    }

    // Setup media
    function _setupMedia() {
      // If there's no media, bail
      if (!player.media) {
        _log('No audio or video element found!', true);
        return false;
      }

      if (player.supported.full) {
        // Remove native video controls
        player.media.removeAttribute('controls');

        // Add type class
        _toggleClass(player.container,
            config.classes.type.replace('{0}', player.type), true);

        // If there's no autoplay attribute, assume the video is stopped and add state class
        _toggleClass(player.container, config.classes.stopped,
            (player.media.getAttribute('autoplay') === null));

        // Add iOS class
        if (player.browser.ios) {
          _toggleClass(player.container, 'ios', true);
        }

        // Inject the player wrapper
        if (player.type === 'video') {
          // Create the wrapper div
          var wrapper = document.createElement('div');
          wrapper.setAttribute('class', config.classes.videoWrapper);

          // Wrap the video in a container
          _wrap(player.media, wrapper);

          // Cache the container
          player.videoContainer = wrapper;
        }
      }

      // YouTube
      if (player.type == 'youtube') {
        _setupYouTube(player.media.getAttribute('data-video-id'));
      }

      // Autoplay
      if (player.media.getAttribute('autoplay') !== null) {
        _play();
      }
    }

    // Setup YouTube
    function _setupYouTube(id) {
      // Remove old containers
      var containers = _getElements('[id^="youtube"]');
      for (var i = containers.length - 1; i >= 0; i--) {
        _remove(containers[i]);
      }

      // Create the YouTube container
      var container = document.createElement('div');
      container.setAttribute('id',
          'youtube-' + Math.floor(Math.random() * (10000)));
      player.media.appendChild(container);

      // Add embed class for responsive
      _toggleClass(player.media, config.classes.videoWrapper, true);
      _toggleClass(player.media, config.classes.embedWrapper, true);

      if (typeof YT === 'object') {
        _YTReady(id, container);
      } else {
        // Load the API
        _injectScript('https://www.youtube.com/iframe_api');

        // Add callback to queue
        callbacks.youtube.push(function () {
          _YTReady(id, container);
        });

        // Setup callback for the API
        window.onYouTubeIframeAPIReady = function () {
          for (var i = callbacks.youtube.length - 1; i >= 0; i--) {
            // Fire callback
            callbacks.youtube[i]();

            // Remove from queue
            callbacks.youtube.splice(i, 1);
          }
        };
      }
    }

    // Handle API ready
    function _YTReady(id, container) {
      _log('YouTube API Ready');

      // Setup timers object
      // We have to poll YouTube for updates
      if (!('timer' in player)) {
        player.timer = {};
      }

      // Setup instance
      // https://developers.google.com/youtube/iframe_api_reference
      player.embed = new YT.Player(container.id, {
        videoId: id,
        playerVars: {
          autoplay: 0,
          controls: (player.supported.full ? 0 : 1),
          rel: 0,
          showinfo: 0,
          iv_load_policy: 3,
          cc_load_policy: (config.captions.defaultActive ? 1 : 0),
          cc_lang_pref: 'en',
          wmode: 'transparent',
          modestbranding: 1,
          disablekb: 1
        },
        events: {
          'onReady': function (event) {
            // Get the instance
            var instance = event.target;

            // Create a faux HTML5 API using the YouTube API
            player.media.play = function () {
              instance.playVideo();
            };
            player.media.pause = function () {
              instance.pauseVideo();
            };
            player.media.stop = function () {
              instance.stopVideo();
            };
            player.media.duration = instance.getDuration();
            player.media.paused = true;
            player.media.currentTime = instance.getCurrentTime();
            player.media.muted = instance.isMuted();

            // Trigger timeupdate
            _triggerEvent(player.media, 'timeupdate');

            // Reset timer
            window.clearInterval(player.timer.buffering);

            // Setup buffering
            player.timer.buffering = window.setInterval(function () {
              // Get loaded % from YouTube
              player.media.buffered = instance.getVideoLoadedFraction();

              // Trigger progress
              _triggerEvent(player.media, 'progress');

              // Bail if we're at 100%
              if (player.media.buffered === 1) {
                window.clearInterval(player.timer.buffering);
              }
            }, 200);

            if (player.supported.full) {
              // Only setup controls once
              if (!player.container.querySelectorAll(
                  config.selectors.controls).length) {
                _setupInterface();
              }

              // Display duration if available
              if (config.displayDuration) {
                _displayDuration();
              }
            }
          },
          'onStateChange': function (event) {
            // Get the instance
            var instance = event.target;

            // Reset timer
            window.clearInterval(player.timer.playing);

            // Handle events
            // -1   Unstarted
            // 0    Ended
            // 1    Playing
            // 2    Paused
            // 3    Buffering
            // 5    Video cued
            switch (event.data) {
              case 0:
                player.media.paused = true;
                _triggerEvent(player.media, 'ended');
                break;

              case 1:
                player.media.paused = false;
                _triggerEvent(player.media, 'play');

                // Poll to get playback progress
                player.timer.playing = window.setInterval(function () {
                  // Set the current time
                  player.media.currentTime = instance.getCurrentTime();

                  // Trigger timeupdate
                  _triggerEvent(player.media, 'timeupdate');
                }, 200);

                break;

              case 2:
                player.media.paused = true;
                _triggerEvent(player.media, 'pause');
            }
          }
        }
      });
    }

    // Setup captions
    function _setupCaptions() {
      if (player.type === 'video') {
        // Inject the container
        player.videoContainer.insertAdjacentHTML('afterbegin',
            '<div class="' + config.selectors.captions.replace('.', '')
            + '"><span></span></div>');

        // Cache selector
        player.captionsContainer = _getElement(
            config.selectors.captions).querySelector('span');

        // Determine if HTML5 textTracks is supported
        player.usingTextTracks = false;
        if (player.media.textTracks) {
          player.usingTextTracks = true;
        }

        // Get URL of caption file if exists
        var captionSrc = '',
            kind,
            children = player.media.childNodes;

        for (var i = 0; i < children.length; i++) {
          if (children[i].nodeName.toLowerCase() === 'track') {
            kind = children[i].kind;
            if (kind === 'captions' || kind === 'subtitles') {
              captionSrc = children[i].getAttribute('src');
            }
          }
        }

        // Record if caption file exists or not
        player.captionExists = true;
        if (captionSrc === '') {
          player.captionExists = false;
          _log('No caption track found.');
        } else {
          _log('Caption track found; URI: ' + captionSrc);
        }

        // If no caption file exists, hide container for caption text
        if (!player.captionExists) {
          _toggleClass(player.container, config.classes.captions.enabled);
        }
        // If caption file exists, process captions
        else {
          // Turn off native caption rendering to avoid double captions
          // This doesn't seem to work in Safari 7+, so the <track> elements are removed from the dom below
          var tracks = player.media.textTracks;
          for (var x = 0; x < tracks.length; x++) {
            tracks[x].mode = 'hidden';
          }

          // Enable UI
          _showCaptions(player);

          // Disable unsupported browsers than report false positive
          if ((player.browser.name === 'IE' && player.browser.version >= 10) ||
              (player.browser.name === 'Firefox' && player.browser.version
                  >= 31) ||
              (player.browser.name === 'Chrome' && player.browser.version >= 43)
              ||
              (player.browser.name === 'Safari' && player.browser.version
                  >= 7)) {
            // Debugging
            _log(
                'Detected unsupported browser for HTML5 captions. Using fallback.');

            // Set to false so skips to 'manual' captioning
            player.usingTextTracks = false;
          }

          // Rendering caption tracks
          // Native support required - http://caniuse.com/webvtt
          if (player.usingTextTracks) {
            _log('TextTracks supported.');

            for (var y = 0; y < tracks.length; y++) {
              var track = tracks[y];

              if (track.kind === 'captions' || track.kind === 'subtitles') {
                _on(track, 'cuechange', function () {
                  // Clear container
                  player.captionsContainer.innerHTML = '';

                  // Display a cue, if there is one
                  if (this.activeCues[0] && this.activeCues[0].hasOwnProperty(
                      'text')) {
                    player.captionsContainer.appendChild(
                        this.activeCues[0].getCueAsHTML().trim());
                  }
                });
              }
            }
          }
          // Caption tracks not natively supported
          else {
            _log('TextTracks not supported so rendering captions manually.');

            // Render captions from array at appropriate time
            player.currentCaption = '';
            player.captions = [];

            if (captionSrc !== '') {
              // Create XMLHttpRequest Object
              var xhr = new XMLHttpRequest();

              xhr.onreadystatechange = function () {
                if (xhr.readyState === 4) {
                  if (xhr.status === 200) {
                    var records = [],
                        record,
                        req = xhr.responseText;

                    records = req.split('\n\n');

                    for (var r = 0; r < records.length; r++) {
                      record = records[r];
                      player.captions[r] = [];
                      player.captions[r] = record.split('\n');
                    }

                    // Remove first element ('VTT')
                    player.captions.shift();

                    _log('Successfully loaded the caption file via AJAX.');
                  } else {
                    _log(
                        'There was a problem loading the caption file via AJAX.',
                        true);
                  }
                }
              };

              xhr.open('get', captionSrc, true);

              xhr.send();
            }
          }

          // If Safari 7+, removing track from DOM [see 'turn off native caption rendering' above]
          if (player.browser.name === 'Safari' && player.browser.version >= 7) {
            _log('Safari 7+ detected; removing track from DOM.');

            // Find all <track> elements
            tracks = player.media.getElementsByTagName('track');

            // Loop through and remove one by one
            for (var t = 0; t < tracks.length; t++) {
              player.media.removeChild(tracks[t]);
            }
          }
        }
      }
    }

    // Setup fullscreen
    function _setupFullscreen() {
      if (player.type != 'audio' && config.fullscreen.enabled) {
        // Check for native support
        var nativeSupport = fullscreen.supportsFullScreen;

        if (nativeSupport || (config.fullscreen.fallback && !_inFrame())) {
          _log(
              (nativeSupport ? 'Native' : 'Fallback') + ' fullscreen enabled.');

          // Add styling hook
          _toggleClass(player.container, config.classes.fullscreen.enabled,
              true);
        } else {
          _log('Fullscreen not supported and fallback disabled.');
        }

        // Toggle state
        _toggleState(player.buttons.fullscreen, false);

        // Set control hide class hook
        if (config.fullscreen.hideControls) {
          _toggleClass(player.container, config.classes.fullscreen.hideControls,
              true);
        }
      }
    }

    // Play media
    function _play() {
      player.media.play();
    }

    // Pause media
    function _pause() {
      player.media.pause();
    }

    // Toggle playback
    function _togglePlay(toggle) {
      // Play
      if (toggle === true) {
        _play();
      }
      // Pause
      else if (toggle === false) {
        _pause();
      }
      // True toggle
      else {
        player.media[player.media.paused ? 'play' : 'pause']();
      }
    }

    // Rewind
    function _rewind(seekTime) {
      // Use default if needed
      if (typeof seekTime !== 'number') {
        seekTime = config.seekTime;
      }
      _seek(player.media.currentTime - seekTime);
    }

    // Fast forward
    function _forward(seekTime) {
      // Use default if needed
      if (typeof seekTime !== 'number') {
        seekTime = config.seekTime;
      }
      _seek(player.media.currentTime + seekTime);
    }

    // Seek to time
    // The input parameter can be an event or a number
    function _seek(input) {
      var targetTime = 0,
          paused = player.media.paused;

      // Explicit position
      if (typeof input === 'number') {
        targetTime = input;
      }
      // Event
      else if (typeof input === 'object' && (input.type === 'input'
          || input.type === 'change')) {
        // It's the seek slider
        // Seek to the selected time
        targetTime = ((input.target.value / input.target.max)
            * player.media.duration);
      }

      // Normalise targetTime
      if (targetTime < 0) {
        targetTime = 0;
      } else if (targetTime > player.media.duration) {
        targetTime = player.media.duration;
      }

      // Set the current time
      // Try/catch incase the media isn't set and we're calling seek() from source() and IE moans
      try {
        player.media.currentTime = targetTime.toFixed(1);
      } catch (e) {
      }

      // YouTube
      if (player.type == 'youtube') {
        player.embed.seekTo(targetTime);

        if (paused) {
          _pause();
        }

        // Trigger timeupdate
        _triggerEvent(player.media, 'timeupdate');
      }

      // Logging
      _log('Seeking to ' + player.media.currentTime + ' seconds');

      // Special handling for 'manual' captions
      _seekManualCaptions(targetTime);
    }

    // Check playing state
    function _checkPlaying() {
      _toggleClass(player.container, config.classes.playing,
          !player.media.paused);
      _toggleClass(player.container, config.classes.stopped,
          player.media.paused);
    }

    // Toggle fullscreen
    function _toggleFullscreen(event) {
      // Check for native support
      var nativeSupport = fullscreen.supportsFullScreen;

      // If it's a fullscreen change event, it's probably a native close
      if (event && event.type === fullscreen.fullScreenEventName) {
        player.isFullscreen = fullscreen.isFullScreen(player.container);
      }
      // If there's native support, use it
      else if (nativeSupport) {
        // Request fullscreen
        if (!fullscreen.isFullScreen(player.container)) {
          fullscreen.requestFullScreen(player.container);
        }
        // Bail from fullscreen
        else {
          fullscreen.cancelFullScreen();
        }

        // Check if we're actually full screen (it could fail)
        player.isFullscreen = fullscreen.isFullScreen(player.container);
      } else {
        // Otherwise, it's a simple toggle
        player.isFullscreen = !player.isFullscreen;

        // Bind/unbind escape key
        if (player.isFullscreen) {
          _on(document, 'keyup', _handleEscapeFullscreen);
          document.body.style.overflow = 'hidden';
        } else {
          _off(document, 'keyup', _handleEscapeFullscreen);
          document.body.style.overflow = '';
        }
      }

      // Set class hook
      _toggleClass(player.container, config.classes.fullscreen.active,
          player.isFullscreen);

      // Set button state
      _toggleState(player.buttons.fullscreen, player.isFullscreen);

      // Toggle controls visibility based on mouse movement and location
      var hoverTimer, isMouseOver = false;

      // Show the player controls
      function _showControls() {
        // Set shown class
        _toggleClass(player.container, config.classes.hover, true);

        // Clear timer every movement
        window.clearTimeout(hoverTimer);

        // If the mouse is not over the controls, set a timeout to hide them
        if (!isMouseOver) {
          hoverTimer = window.setTimeout(function () {
            _toggleClass(player.container, config.classes.hover, false);
          }, 2000);
        }
      }

      // Check mouse is over the controls
      function _setMouseOver(event) {
        isMouseOver = (event.type === 'mouseenter');
      }

      if (config.fullscreen.hideControls) {
        // Hide on entering full screen
        _toggleClass(player.controls, config.classes.hover, false);

        // Keep an eye on the mouse location in relation to controls
        _toggleHandler(player.controls, 'mouseenter mouseleave', _setMouseOver,
            player.isFullscreen);

        // Show the controls on mouse move
        _toggleHandler(player.container, 'mousemove', _showControls,
            player.isFullscreen);
      }
    }

    // Bail from faux-fullscreen
    function _handleEscapeFullscreen(event) {
      // If it's a keypress and not escape, bail
      if ((event.which || event.charCode || event.keyCode) === 27
          && player.isFullscreen) {
        _toggleFullscreen();
      }
    }

    // Set volume
    function _setVolume(volume) {
      // Use default if no value specified
      if (typeof volume === 'undefined') {
        if (config.storage.enabled && _storage().supported) {
          volume = window.localStorage[config.storage.key] || config.volume;
        } else {
          volume = config.volume;
        }
      }

      // Maximum is 10
      if (volume > 10) {
        volume = 10;
      }
      // Minimum is 0
      if (volume < 0) {
        volume = 0;
      }

      // Set the player volume
      player.media.volume = parseFloat(volume / 10);

      // YouTube
      if (player.type == 'youtube') {
        player.embed.setVolume(player.media.volume * 100);

        // Trigger timeupdate
        _triggerEvent(player.media, 'volumechange');
      }

      // Toggle muted state
      if (player.media.muted && volume > 0) {
        _toggleMute();
      }
    }

    // Mute
    function _toggleMute(muted) {
      // If the method is called without parameter, toggle based on current value
      if (typeof muted !== 'boolean') {
        muted = !player.media.muted;
      }

      // Set button state
      _toggleState(player.buttons.mute, muted);

      // Set mute on the player
      player.media.muted = muted;

      // YouTube
      if (player.type === 'youtube') {
        player.embed[player.media.muted ? 'mute' : 'unMute']();

        // Trigger timeupdate
        _triggerEvent(player.media, 'volumechange');
      }
    }

    // Update volume UI and storage
    function _updateVolume() {
      // Get the current volume
      var volume = player.media.muted ? 0 : (player.media.volume * 10);

      // Update the <input type="range"> if present
      if (player.supported.full && player.volume) {
        player.volume.value = volume;
      }

      // Store the volume in storage
      if (config.storage.enabled && _storage().supported) {
        window.localStorage.setItem(config.storage.key, volume);
      }

      // Toggle class if muted
      _toggleClass(player.container, config.classes.muted, (volume === 0));

      // Update checkbox for mute state
      if (player.supported.full && player.buttons.mute) {
        _toggleState(player.buttons.mute, (volume === 0));
      }
    }

    // Toggle captions
    function _toggleCaptions(show) {
      // If there's no full support, or there's no caption toggle
      if (!player.supported.full || !player.buttons.captions) {
        return;
      }

      // If the method is called without parameter, toggle based on current value
      if (typeof show !== 'boolean') {
        show = (player.container.className.indexOf(
            config.classes.captions.active) === -1);
      }

      // Toggle state
      _toggleState(player.buttons.captions, show);

      // Add class hook
      _toggleClass(player.container, config.classes.captions.active, show);
    }

    // Check if media is loading
    function _checkLoading(event) {
      var loading = (event.type === 'waiting');

      // Clear timer
      clearTimeout(player.loadingTimer);

      // Timer to prevent flicker when seeking
      player.loadingTimer = setTimeout(function () {
        _toggleClass(player.container, config.classes.loading, loading);
      }, (loading ? 250 : 0));
    }

    // Update <progress> elements
    function _updateProgress(event) {
      var progress = player.progress.played.bar,
          text = player.progress.played.text,
          value = 0;

      if (event) {
        switch (event.type) {
            // Video playing
          case 'timeupdate':
          case 'seeking':
            value = _getPercentage(player.media.currentTime,
                player.media.duration);

            // Set seek range value only if it's a 'natural' time event
            if (event.type == 'timeupdate' && player.buttons.seek) {
              player.buttons.seek.value = value;
            }

            break;

            // Events from seek range
          case 'change':
          case 'input':
            value = event.target.value;
            break;


            // Check buffer status
          case 'playing':
          case 'progress':
            progress = player.progress.buffer.bar;
            text = player.progress.buffer.text;
            value = (function () {
              var buffered = player.media.buffered;

              // HTML5
              if (buffered && buffered.length) {
                return _getPercentage(buffered.end(0), player.media.duration);
              }
              // YouTube returns between 0 and 1
              else if (typeof buffered === 'number') {
                return (buffered * 100);
              }

              return 0;
            })();
        }
      }

      // Set values
      if (progress) {
        progress.value = value;
      }
      if (text) {
        text.innerHTML = value;
      }
    }

    // Update the displayed time
    function _updateTimeDisplay(time, element) {
      // Bail if there's no duration display
      if (!element) {
        return;
      }

      player.secs = parseInt(time % 60);
      player.mins = parseInt((time / 60) % 60);
      player.hours = parseInt(((time / 60) / 60) % 60);

      // Do we need to display hours?
      var displayHours = (parseInt(((player.media.duration / 60) / 60) % 60)
          > 0);

      // Ensure it's two digits. For example, 03 rather than 3.
      player.secs = ('0' + player.secs).slice(-2);
      player.mins = ('0' + player.mins).slice(-2);

      // Render
      element.innerHTML = (displayHours ? player.hours + ':' : '') + player.mins
          + ':' + player.secs;
    }

    // Show the duration on metadataloaded
    function _displayDuration() {
      var duration = player.media.duration || 0;

      // If there's only one time display, display duration there
      if (!player.duration && config.displayDuration && player.media.paused) {
        _updateTimeDisplay(duration, player.currentTime);
      }

      // If there's a duration element, update content
      if (player.duration) {
        _updateTimeDisplay(duration, player.duration);
      }
    }

    // Handle time change event
    function _timeUpdate(event) {
      // Duration
      _updateTimeDisplay(player.media.currentTime, player.currentTime);

      // Playing progress
      _updateProgress(event);
    }

    // Remove <source> children and src attribute
    function _removeSources() {
      // Find child <source> elements
      var sources = player.media.querySelectorAll('source');

      // Remove each
      for (var i = sources.length - 1; i >= 0; i--) {
        _remove(sources[i]);
      }

      // Remove src attribute
      player.media.removeAttribute('src');
    }

    // Inject a source
    function _addSource(attributes) {
      if (attributes.src) {
        // Create a new <source>
        var element = document.createElement('source');

        // Set all passed attributes
        _setAttributes(element, attributes);

        // Inject the new source
        _prependChild(player.media, element);
      }
    }

    // Update source
    // Sources are not checked for support so be careful
    function _parseSource(sources) {
      // YouTube
      if (player.type === 'youtube' && typeof sources === 'string') {
        // Destroy YouTube instance
        player.embed.destroy();

        // Re-setup YouTube
        // We don't use loadVideoBy[x] here since it has issues
        _setupYouTube(sources);

        // Update times
        _timeUpdate();

        // Bail
        return;
      }

      // Pause playback (webkit freaks out)
      _pause();

      // Restart
      _seek();

      // Remove current sources
      _removeSources();

      // If a single source is passed
      // .source('path/to/video.mp4')
      if (typeof sources === 'string') {
        _addSource({
          src: sources
        });
      }

      // An array of source objects
      // Check if a source exists, use that or set the 'src' attribute?
      // .source([{ src: 'path/to/video.mp4', type: 'video/mp4' },{ src: 'path/to/video.webm', type: 'video/webm' }])
      else if (sources.constructor === Array) {
        for (var index in sources) {
          _addSource(sources[index]);
        }
      }

      if (player.supported.full) {
        // Reset time display
        _timeUpdate();

        // Update the UI
        _checkPlaying();
      }

      // Re-load sources
      player.media.load();

      // Play if autoplay attribute is present
      if (player.media.getAttribute('autoplay') !== null) {
        _play();
      }
    }

    // Update poster
    function _updatePoster(source) {
      if (player.type === 'video') {
        player.media.setAttribute('poster', source);
      }
    }

    // Listen for events
    function _listeners() {
      // IE doesn't support input event, so we fallback to change
      var inputEvent = (player.browser.name == 'IE' ? 'change' : 'input');

      // Detect tab focus
      function checkFocus() {
        var focused = document.activeElement;
        if (!focused || focused == document.body) {
          focused = null;
        } else if (document.querySelector) {
          focused = document.querySelector(':focus');
        }
        for (var button in player.buttons) {
          var element = player.buttons[button];

          _toggleClass(element, 'tab-focus', (element === focused));
        }
      }

      _on(window, 'keyup', function (event) {
        var code = (event.keyCode ? event.keyCode : event.which);

        if (code == 9) {
          checkFocus();
        }
      });
      for (var button in player.buttons) {
        var element = player.buttons[button];

        _on(element, 'blur', function () {
          _toggleClass(element, 'tab-focus', false);
        });
      }

      // Play
      _on(player.buttons.play, 'click', function () {
        _play();
        setTimeout(function () {
          player.buttons.pause.focus();
        }, 100);
      });

      // Pause
      _on(player.buttons.pause, 'click', function () {
        _pause();
        setTimeout(function () {
          player.buttons.play.focus();
        }, 100);
      });

      // Restart
      _on(player.buttons.restart, 'click', _seek);

      // Rewind
      _on(player.buttons.rewind, 'click', _rewind);

      // Fast forward
      _on(player.buttons.forward, 'click', _forward);

      // Seek
      _on(player.buttons.seek, inputEvent, _seek);

      // Set volume
      _on(player.volume, inputEvent, function () {
        _setVolume(this.value);
      });

      // Mute
      _on(player.buttons.mute, 'click', _toggleMute);

      // Fullscreen
      _on(player.buttons.fullscreen, 'click', _toggleFullscreen);

      // Handle user exiting fullscreen by escaping etc
      if (fullscreen.supportsFullScreen) {
        _on(document, fullscreen.fullScreenEventName, _toggleFullscreen);
      }

      // Time change on media
      _on(player.media, 'timeupdate seeking', _timeUpdate);

      // Update manual captions
      _on(player.media, 'timeupdate', _seekManualCaptions);

      // Display duration
      _on(player.media, 'loadedmetadata', _displayDuration);

      // Captions
      _on(player.buttons.captions, 'click', _toggleCaptions);

      // Handle the media finishing
      _on(player.media, 'ended', function () {
        // Clear
        if (player.type === 'video') {
          player.captionsContainer.innerHTML = '';
        }

        // Reset UI
        _checkPlaying();
      });

      // Check for buffer progress
      _on(player.media, 'progress playing', _updateProgress);

      // Handle native mute
      _on(player.media, 'volumechange', _updateVolume);

      // Handle native play/pause
      _on(player.media, 'play pause', _checkPlaying);

      // Loading
      _on(player.media, 'waiting canplay seeked', _checkLoading);

      // Click video
      if (player.type === 'video' && config.click) {
        _on(player.videoContainer, 'click', function () {
          if (player.media.paused) {
            _triggerEvent(player.buttons.play, 'click');
          } else if (player.media.ended) {
            _seek();
            _triggerEvent(player.buttons.play, 'click');
          } else {
            _triggerEvent(player.buttons.pause, 'click');
          }
        });
      }
    }

    // Destroy an instance
    // Event listeners are removed when elements are removed
    // http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
    function _destroy() {
      // Bail if the element is not initialized
      if (!player.init) {
        return null;
      }

      // Reset container classname
      player.container.setAttribute('class',
          config.selectors.container.replace('.', ''));

      // Remove init flag
      player.init = false;

      // Remove controls
      _remove(_getElement(config.selectors.controls));

      // YouTube
      if (player.type === 'youtube') {
        player.embed.destroy();
        return;
      }

      // If video, we need to remove some more
      if (player.type === 'video') {
        // Remove captions
        _remove(_getElement(config.selectors.captions));

        // Remove video wrapper
        _unwrap(player.videoContainer);
      }

      // Restore native video controls
      player.media.setAttribute('controls', '');

      // Clone the media element to remove listeners
      // http://stackoverflow.com/questions/19469881/javascript-remove-all-event-listeners-of-specific-type
      var clone = player.media.cloneNode(true);
      player.media.parentNode.replaceChild(clone, player.media);
    }

    // Setup a player
    function _init() {
      // Bail if the element is initialized
      if (player.init) {
        return null;
      }

      // Setup the fullscreen api
      fullscreen = _fullscreen();

      // Sniff out the browser
      player.browser = _browserSniff();

      // Get the media element
      player.media = player.container.querySelectorAll('audio, video, div')[0];

      // Set media type
      var tagName = player.media.tagName.toLowerCase();
      if (tagName === 'div') {
        player.type = player.media.getAttribute('data-type');
      } else {
        player.type = tagName;
      }

      // Check for full support
      player.supported = api.supported(player.type);

      // If no native support, bail
      if (!player.supported.basic) {
        return false;
      }

      // Debug info
      _log(player.browser.name + ' ' + player.browser.version);

      // Setup media
      _setupMedia();

      // Setup interface
      if (player.type == 'video' || player.type == 'audio') {
        // Bail if no support
        if (!player.supported.full) {
          // Successful setup
          player.init = true;

          // Don't inject controls if no full support
          return;
        }

        // Setup UI
        _setupInterface();

        // Display duration if available
        if (config.displayDuration) {
          _displayDuration();
        }

        // Set up aria-label for Play button with the title option
        _setupPlayAria();
      }

      // Successful setup
      player.init = true;
    }

    function _setupInterface() {
      // Inject custom controls
      _injectControls();

      // Find the elements
      if (!_findElements()) {
        return false;
      }

      // Captions
      _setupCaptions();

      // Set volume
      _setVolume();
      _updateVolume();

      // Setup fullscreen
      _setupFullscreen();

      // Listeners
      _listeners();
    }

    // Initialize instance
    _init();

    // If init failed, return an empty object
    if (!player.init) {
      return {};
    }

    return {
      media: player.media,
      play: _play,
      pause: _pause,
      restart: _seek,
      rewind: _rewind,
      forward: _forward,
      seek: _seek,
      source: _parseSource,
      poster: _updatePoster,
      setVolume: _setVolume,
      togglePlay: _togglePlay,
      toggleMute: _toggleMute,
      toggleCaptions: _toggleCaptions,
      toggleFullscreen: _toggleFullscreen,
      isFullscreen: function () {
        return player.isFullscreen || false;
      },
      support: function (mimeType) {
        return _supportMime(player, mimeType);
      },
      destroy: _destroy,
      restore: _init
    };
  }

  // Check for support
  api.supported = function (type) {
    var browser = _browserSniff(),
        oldIE = (browser.name === 'IE' && browser.version <= 9),
        iPhone = /iPhone|iPod/i.test(navigator.userAgent),
        audio = !!document.createElement('audio').canPlayType,
        video = !!document.createElement('video').canPlayType,
        basic, full;

    switch (type) {
      case 'video':
        basic = video;
        full = (basic && (!oldIE && !iPhone));
        break;

      case 'audio':
        basic = audio;
        full = (basic && !oldIE);
        break;

      case 'youtube':
        basic = true;
        full = (!oldIE && !iPhone);
        break;

      default:
        basic = (audio && video);
        full = (basic && !oldIE);
    }

    return {
      basic: basic,
      full: full
    };
  };

  // Expose setup function
  api.setup = function (options) {
    // Extend the default options with user specified
    config = _extend(defaults, options);

    // Bail if disabled or no basic support
    // You may want to disable certain UAs etc
    if (!config.enabled || !api.supported().basic) {
      return false;
    }

    // Get the players
    var elements = document.querySelectorAll(config.selectors.container),
        players = [];

    // Create a player instance for each element
    for (var i = elements.length - 1; i >= 0; i--) {
      // Get the current element
      var element = elements[i];

      // Setup a player instance and add to the element
      if (typeof element.plyr === 'undefined') {
        // Create new instance
        var instance = new Plyr(element);

        // Set plyr to false if setup failed
        element.plyr = (Object.keys(instance).length ? instance : false);

        // Callback
        if (typeof config.onSetup === 'function') {
          config.onSetup.apply(element.plyr);
        }
      }

      // Add to return array even if it's already setup
      players.push(element.plyr);
    }

    return players;
  };

}(this.plyr = this.plyr || {}));
